package org.leafstar.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.leafstar.shortlink.project.common.convention.exception.ClientException;
import org.leafstar.shortlink.project.common.convention.exception.ServiceException;
import org.leafstar.shortlink.project.common.enums.VailDatetypeEnum;
import org.leafstar.shortlink.project.dao.entity.*;
import org.leafstar.shortlink.project.dao.mapper.*;
import org.leafstar.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import org.leafstar.shortlink.project.dto.req.LinkCreateReqDTO;
import org.leafstar.shortlink.project.dto.req.LinkPageReqDTO;
import org.leafstar.shortlink.project.dto.req.LinkUpdateReqDTO;
import org.leafstar.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import org.leafstar.shortlink.project.dto.resp.*;
import org.leafstar.shortlink.project.mq.DelayShortLinkStatsProducer;
import org.leafstar.shortlink.project.service.LinkService;
import org.leafstar.shortlink.project.util.HashUtil;
import lombok.extern.slf4j.Slf4j;
import org.leafstar.shortlink.project.util.LinkUtil;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.leafstar.shortlink.project.common.constant.RedisKeyConstant.*;
import static org.leafstar.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;
import static org.leafstar.shortlink.project.common.constant.ShortLinkConstant.LOCK_GID_UPDATE_KEY;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDO> implements LinkService {
    private final RBloomFilter<String> linkCreateBloomFilter;
    private final LinkRouteMapper linkRouteMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final RBloomFilter<String> shortUriCreateBloomFilter;

    //监控的各种mapper
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;

    private final LinkAccessLogsMapper linkAccessLogsMapper;

    private final DelayShortLinkStatsProducer delayShortLinkStatsProducer;


    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    //    @Value("${short-link.domain.default}")
    private final String createShortLinkDefaultDomain="http://";

    //批量创建短链接
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            LinkCreateReqDTO linkCreateReqDTO = BeanUtil.toBean(requestParam, LinkCreateReqDTO.class);
            linkCreateReqDTO.setOriginUrl(originUrls.get(i));
            linkCreateReqDTO.setDescribe(describes.get(i));
            try {
                LinkCreateRespDTO shortLink = createShortLink(linkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }
    @Override
    @Transactional
    public LinkCreateRespDTO createShortLink(LinkCreateReqDTO requestParam) {
        if(requestParam.getDomain()==null){
            requestParam.setDomain("y.com");
        }
        LinkDO linkDO = BeanUtil.copyProperties(requestParam, LinkDO.class);
        linkDO.setEnableStatus(0);
        String suffix;
        while (true) {
//            一般不会触发
//            if (count >= 10) throw new ServiceException("当前使用人数过多，请稍后重试");
            suffix = generateSuffix(requestParam);
            linkDO.setShortUri(suffix);
            linkDO.setFavicon(getFavicon(requestParam.getOriginUrl()));
            linkDO.setFullShortUrl(StrBuilder.create(requestParam.getDomain()).append("/").append(suffix).toString());
            try {
                baseMapper.insert(linkDO);
                //插入对应的路由表
                linkRouteMapper.insert(LinkRoute.builder()
                        .gid(linkDO.getGid())
                        .fullShortUrl(linkDO.getFullShortUrl())
                        .build()
                );
                //进行缓存预热
                stringRedisTemplate.opsForValue().set(
                        String.format(REDIRECT_LINK_KEY, linkDO.getFullShortUrl()),
                        linkDO.getOriginUrl(),
                        LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),
                        TimeUnit.MILLISECONDS
                );
                break;
            } catch (DuplicateKeyException ex) {
                log.warn("布隆过滤器验证错误,可能是redis或者高并发导致问题:" + linkDO.getFullShortUrl());
            } finally {
                //不管是高并发或者redis，都重新插入过滤器
                linkCreateBloomFilter.add(linkDO.getFullShortUrl());
            }
        }
        return LinkCreateRespDTO.builder()
                .fullShortUrl(linkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @Override
    public IPage<LinkPageRespDTO> pageLink(LinkPageReqDTO requestParam) {
        if(requestParam.getGid()==null)return null;
        IPage<LinkDO> resultPage = baseMapper.pageLink(requestParam);

        return resultPage.convert(each -> {
            LinkPageRespDTO result = BeanUtil.toBean(each, LinkPageRespDTO.class);
            result.setDomain(result.getDomain());
            return result;
        });
    }

    /**
     * 查询短链接分组内的链接数量
     *
     * @param requestParam gids
     * @return LinkGroupCountQueryRespDTO列表
     */
    @Override
    public List<LinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<LinkDO> queryWrapper = new QueryWrapper<LinkDO>()
                .select("gid,count(*) shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps, LinkGroupCountQueryRespDTO.class);
    }

    @Override
    public void updateShortLink(LinkUpdateReqDTO requestParam) {
        //该逻辑大概是有问题的，暂未验证
        // TODO:验证可行性
        LambdaQueryWrapper<LinkDO> queryWrapper = new LambdaQueryWrapper<LinkDO>()
                .eq(LinkDO::getGid, requestParam.getGid())
                .eq(LinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0);
        LinkDO hasLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasLinkDO == null) throw new ClientException("短链接不存在");

        LambdaUpdateWrapper<LinkDO> updateWrapper = new LambdaUpdateWrapper<LinkDO>()
                .eq(LinkDO::getGid, hasLinkDO.getGid())
                .eq(LinkDO::getFullShortUrl, hasLinkDO.getFullShortUrl())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0)
                .set(Objects.equals(requestParam.getValidDateType(), VailDatetypeEnum.PERMANENT.getType()),
                        LinkDO::getValidDate, null);
        if (Objects.equals(hasLinkDO.getGid(), requestParam.getGid())) {
            BeanUtil.copyProperties(requestParam, hasLinkDO);
            //没有修改分组
            baseMapper.update(hasLinkDO, updateWrapper);
        } else {
            //修改分组
            baseMapper.delete(updateWrapper);
            BeanUtil.copyProperties(requestParam, hasLinkDO);
            baseMapper.insert(hasLinkDO);
        }
    }

    /**
     * 短链接重定向，即核心跳转功能
     *
     * @param shortUri suffix
     * @param request  请求
     * @param response 返回
     */
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String fullShortUrl = serverName + "/" + shortUri;
        String originUrl = stringRedisTemplate.opsForValue().get(String.format(REDIRECT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originUrl)) {
            //缓存中存在原始链接
            //统计短链接监控数据
            ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, recordDTO);
            ((HttpServletResponse) response).sendRedirect( originUrl);
            return;
        }
        boolean contains = shortUriCreateBloomFilter.contains(fullShortUrl);
        if (!contains) {
            //布隆过滤器中不包含该短链接，跳转404
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        String redirectIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(REDIRECT_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(redirectIsNullShortLink)) {
            //但数据库中不存在短链接时，会被放到该缓存中，如果不为空，说明数据库中没有，则直接返回
            //TODO:生成该短链接时，应该将其删除，半分钟的存活时间，也可以不实现
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        //redis里面为空
        RLock lock = redissonClient.getLock(String.format(LOCK_REDIRECT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            //进行双重判定
            originUrl = stringRedisTemplate.opsForValue().get(String.format(REDIRECT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originUrl)) {
                ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, null, recordDTO);
                ((HttpServletResponse) response).sendRedirect( originUrl);
                return;
            }
            LambdaQueryWrapper<LinkRoute> queryWrapper = new LambdaQueryWrapper<LinkRoute>()
                    .eq(LinkRoute::getFullShortUrl, fullShortUrl);
            LinkRoute linkRoute = linkRouteMapper.selectOne(queryWrapper);
            if (linkRoute == null) {
                //将其加入指定缓存
                stringRedisTemplate.opsForValue().set(String.format(REDIRECT_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "#", 30, TimeUnit.SECONDS);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LinkDO linkDO = baseMapper.selectOne(new LambdaUpdateWrapper<LinkDO>()
                    .eq(LinkDO::getGid, linkRoute.getGid())
                    .eq(LinkDO::getFullShortUrl, fullShortUrl)
                    .eq(LinkDO::getDelFlag, 0)
                    .eq(LinkDO::getEnableStatus, 0));
            if (linkDO == null || linkDO.getValidDate().before(new Date())) {
                //数据库中的短链接已经过期
                stringRedisTemplate.opsForValue().set(String.format(REDIRECT_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "#", 30, TimeUnit.SECONDS);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            //将其存入缓存
            stringRedisTemplate.opsForValue().set(
                    String.format(REDIRECT_LINK_KEY, fullShortUrl),
                    linkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
            //协议可以由用户自己传递过来
            ShortLinkStatsRecordDTO recordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, linkDO.getGid(), recordDTO);
            ((HttpServletResponse) response).sendRedirect( linkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    private String generateSuffix(LinkCreateReqDTO reqDTO) {
        //从布隆过滤器中先判断一遍
        int count = 0;
        String suffix;
        while (true) {
            if (count >= 10) throw new ServiceException("同一链接生成次数过多，请稍后重试");
            //每次修改一点内容，防止再次生成不可用链接，但是同一链接只有10次机会
            suffix = HashUtil.hashToBase62(reqDTO.getOriginUrl() + count);
            if (!linkCreateBloomFilter.contains(reqDTO.getDomain()+"/"+suffix)) {
                //使用这种方法，布隆过滤器可能误判，但是返回的值一定是布隆过滤器中不存在的
                return suffix;
            }
            count++;
        }
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.setConnectTimeout(1000);
        connection.setReadTimeout(1000);
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 监控的各个指标，方便其他调用
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        //保证线程安全
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        //可以理解为保证原子性和高并发的String
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
        };
        //如果cookie不为空
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                                uv.set(each);
                                Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                                uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                            },
                            //cookie值为空
                            addResponseCookieTask);
        } else {
            //如果cookie为空
            addResponseCookieTask.run();
        }
        //获得各种属性信息
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        //将用户ip添加到redis
        Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }

    //    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
        fullShortUrl = Optional.ofNullable(fullShortUrl).orElse(statsRecord.getFullShortUrl());
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, fullShortUrl));
        RLock rLock = readWriteLock.readLock();
        if (!rLock.tryLock()) {
            //说明此刻访问较多，推迟记录
            delayShortLinkStatsProducer.send(statsRecord);
            return;
        }
        try {
            if (StrUtil.isBlank(gid)) {
                LambdaQueryWrapper<LinkRoute> queryWrapper = new LambdaQueryWrapper<LinkRoute>()
                        .eq(LinkRoute::getFullShortUrl, fullShortUrl);
                LinkRoute linkRoute = linkRouteMapper.selectOne(queryWrapper);
                gid = linkRoute.getGid();
            }
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .uip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .build();
            //插入，失败则更新
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            //获得地图相关信息
            Map<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", statsLocaleAmapKey);
            localeParamMap.put("ip", statsRecord.getRemoteAddr());
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infoCode = localeResultObj.getString("infocode");
            String actualProvince = "未知";
            String actualCity = "未知";
            if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .province(actualProvince = unknownFlag ? actualProvince : province)
                        .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .country("中国")
                        .gid(gid)
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
            }
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .os(statsRecord.getOs())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .browser(statsRecord.getBrowser())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(statsRecord.getDevice())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(statsRecord.getNetwork())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(statsRecord.getUv())
                    .ip(statsRecord.getRemoteAddr())
                    .browser(statsRecord.getBrowser())
                    .os(statsRecord.getOs())
                    .network(statsRecord.getNetwork())
                    .device(statsRecord.getDevice())
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            baseMapper.incrementStats(gid, fullShortUrl, 1, statsRecord.getUvFirstFlag() ? 1 : 0, statsRecord.getUipFirstFlag() ? 1 : 0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .todayPv(1)
                    .todayUv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .todayUip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        } catch (Throwable ex) {
            log.error("短链接访问量统计异常", ex);
        } finally {
            rLock.unlock();
        }
    }

}
